package com.p6spy.engine.spy.option;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import com.p6spy.engine.common.ClassHasher;
import com.p6spy.engine.common.CustomHashedHashSet;
import com.p6spy.engine.common.P6Util;
import com.p6spy.engine.logging.Category;
import com.p6spy.engine.spy.P6Factory;

public class P6OptionsRepository {

    private final Map<String, Object> map = new HashMap<String, Object>();

    private Set<DelayedOptionChange> delayedOptionChanges = new HashSet<DelayedOptionChange>();

    private final List<P6OptionChangedListener> listeners = new ArrayList<P6OptionChangedListener>();

    /**
     * Inidicator whether initialization has been completed. To prevent usage of not yet initialized
     * properties.
     */
    private boolean initCompleted = false;

    public void initCompleted() {
        this.initCompleted = true;
        fireDelayedOptionChanges();
    }

    public <T> boolean set(Class<T> type, String key, Object value) {
        if (value == null) {
            return false;
        }

        return setOrUnSet(type, key, value, null);
    }

    public <T> boolean setOrUnSet(Class<T> type, String key, Object value, Object defaultValue) {
        if (key == null || key.isEmpty()) {
            throw new IllegalArgumentException("key can be neither null nor empty!");
        }

        if (value == null) {
            value = defaultValue;
        }

        if (value == null) {
            setInternal(key, value);
        } else {
            setInternal(key, parse(type, value));
        }

        return true;
    }

    <T> Object parse(Class<T> type, Object value) {
        if (type.isAssignableFrom(Boolean.class)) {
            return P6Util.isTrue(value.toString(), true);
        } else if (type.isAssignableFrom(String.class)) {
            return value.toString();
        } else if (type.isAssignableFrom(Long.class)) {
            return Long.parseLong(value.toString());
        } else if (type.isAssignableFrom(Integer.class)) {
            return Integer.parseInt(value.toString());
        } else if (type.isAssignableFrom(Set.class)) {
            throw new IllegalArgumentException("please call the setSet() method instead!");
        } else if (type.isAssignableFrom(Collection.class) || type.isAssignableFrom(List.class)) {
            throw new IllegalArgumentException("type not supported:" + type.getName());
        } else if (type.isAssignableFrom(Pattern.class)) {
            return Pattern.compile(value.toString());
        } else if (type.isAssignableFrom(Category.class)) {
            return new Category(value.toString());
        } else {
//		if (type.isEnum()) {
//	    	// is sufficient for our use case where toString returns enum name
//	    	for (T enumConstant : type.getEnumConstants()) {
//	    	   if (enumConstant.toString().equalsIgnoreCase(value.toString())) {
//	    		   return enumConstant;
//	    	   }
//	    	}
//		}

            Object instance;
            try {
                instance = P6Util.forName(value.toString()).newInstance();
            } catch (Exception ex) {
                // try one more hack to load the thing
                try {
                    ClassLoader loader = ClassLoader.getSystemClassLoader();
                    instance = loader.loadClass(value.toString()).newInstance();
                } catch (Exception e) {
                    System.err.println("Cannot instantiate " + value + ", even on second attempt. ");
                    e.printStackTrace(System.err);
                    return null;
                }
            }

            try {
                return instance;
            } catch (ClassCastException e) {
                System.err.println("Value " + value + ", is not of expected type. Error: " + e);
                return null;
            }
        }
    }

    void setInternal(String key, Object value) {
        final Object oldValue = map.put(key, value);

        // propagate the changes
        fireOptionChanged(key, oldValue, value);
    }

    @SuppressWarnings("unchecked")
    public <T> boolean setSet(Class<T> type, String key, String csv) {
        if (csv == null) {
            return false;
        }

        final List<String> collection = P6Util.parseCSVList(csv);

        if (collection == null) {
            return false;
        }

        final Set<T> oldValue = getSet(type, key);
        Set<T> newValue = null;

        if (collection.isEmpty()) {
            map.remove(key);
        } else {
            if (type.equals(P6Factory.class)) {
                // for P6Factories the hashcode is computed based on class
                newValue = new CustomHashedHashSet<T>(new ClassHasher());
            } else {
                newValue = new HashSet<T>();
            }

            for (String item : collection) {

                if (item.startsWith("-")) {
                    throw new IllegalArgumentException("- prefix has been deprecated for list-like properties! Full overriding happens (see: http://p6spy.github.io/p6spy/2.0/configandusage.html)");
                }

                newValue.add((T) parse(type, item));
            }
            map.put(key, newValue);
        }

        // propagate the changes
        fireOptionChanged(key, oldValue, newValue);

        return true;
    }

    void fireOptionChanged(final String key, final Object oldValue, final Object newValue) {
        if (initCompleted) {
            fireDelayedOptionChanges();
            for (P6OptionChangedListener listener : listeners) {
                listener.optionChanged(key, oldValue, newValue);
            }
        } else {
            delayedOptionChanges.add(new DelayedOptionChange(key, oldValue, newValue));
        }
    }

    private synchronized void fireDelayedOptionChanges() {
        if (null == delayedOptionChanges) {
            return;
        }

        for (DelayedOptionChange delayedOption : delayedOptionChanges) {
            for (P6OptionChangedListener listener : listeners) {
                listener.optionChanged(delayedOption.getKey(), delayedOption.getOldValue(),
                        delayedOption.getNewValue());
            }
        }

        // make sure the delayed options get cleared to continue normal processing
        delayedOptionChanges = null;
    }

    @SuppressWarnings("unchecked")
    public <T> T get(Class<T> type, String key) {
        if (!initCompleted) {
            throw new IllegalStateException("Options didn't load completely, yet!");
        }
        return (T) map.get(key);
    }

    @SuppressWarnings("unchecked")
    public <T> Set<T> getSet(Class<T> type, String key) {
        return (Set<T>) map.get(key);
    }

    public void registerOptionChangedListener(P6OptionChangedListener listener) {
        if (listener == null) {
            throw new IllegalArgumentException("P6OptionChangedListener can't be null!");
        }

        this.listeners.add(listener);
    }

    public void unregisterOptionChangedListener(P6OptionChangedListener listener) {
        if (listener == null) {
            throw new IllegalArgumentException("P6OptionChangedListener can't be null!");
        }

        this.listeners.remove(listener);
    }

    class DelayedOptionChange {

        private final String key;
        private final Object oldValue;
        private final Object newValue;

        public DelayedOptionChange(String key, Object oldValue, Object newValue) {
            super();

            if (null == key || key.isEmpty()) {
                throw new IllegalArgumentException("key can be neither null nor empty!");
            }

            this.key = key;
            this.oldValue = oldValue;
            this.newValue = newValue;
        }


        @Override
        public int hashCode() {
            // this is important part!
            return key.hashCode();
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            DelayedOptionChange other = (DelayedOptionChange) obj;
            if (!getOuterType().equals(other.getOuterType()))
                return false;
            if (key == null) {
              return other.key == null;
            } else return key.equals(other.key);
        }

        public String getKey() {
            return key;
        }

        public Object getOldValue() {
            return oldValue;
        }

        public Object getNewValue() {
            return newValue;
        }

        private P6OptionsRepository getOuterType() {
            return P6OptionsRepository.this;
        }
    }
}
